# coding=utf-8
'''
Created on 2017年8月20日

@author: Louyj
'''

import json
import socket
import time
import urllib
import base64
import traceback

from abc import abstractmethod
from time import sleep

from redis import StrictRedis
from redis.sentinel import Sentinel

from kazoo.client import KazooClient
from kazoo.client import NodeExistsError
from kazoo.retry import KazooRetry
from kazoo.client import KazooState
import mj_crawler.Lib.Logger.log4py as logging
from mj_crawler.mj_parser import common_errors


class CrawlerRegister(object):
    messages = {
        '30': "成功",
        '31': "vin长度无效",
        '32': "无效的vin",
        '41': "TCP 连线错误",
        '82': "登入失败",
        '93': "无内容",
        '97': "爬虫不支持的VIN",
        '98': "ETKA 错误",
        '99': "中断"
    }

    def __init__(self, brand, zk_connect, resource_type, redis_master=None, redis_password=None, redis_host=None,
                 redis_port=6379, redis_sentinels=None, redis_timeout=10, parallelism=10, daily_limit=100, name=None,
                 hourly_limit=None):
        self.logger = logging.get_logger("crawler", "CrawlerRegister")
        self.base_zk_Path = "/crawler/register"
        self.zk_connect = zk_connect
        self.resource_type = resource_type
        self.redis_master = redis_master
        self.redis_password = redis_password
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_sentinels = redis_sentinels
        self.redis_timeout = redis_timeout

        self.name = name if name else urllib.parse.quote(self.get_ip() + '-1')
        self.brand = brand

        self.vinRequestMapper = {}
        self.vinStartTimeMapper = {}

        self.zk_data = {'ip': CrawlerRegister.get_ip(), 'type': self.resource_type, 'name': self.name, 'brand': brand,
                        'parallelism': parallelism, 'dailyLimit': daily_limit, 'hourlyLimit': hourly_limit,
                        "startTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}
        self.need_reset = False

        self.init_redis()
        self.register()

    def init_redis(self):
        if self.redis_sentinels:
            sentinel = Sentinel(self.redis_sentinels, password=self.redis_password, socket_timeout=self.redis_timeout)
            self.redis = sentinel.master_for(self.redis_master, socket_timeout=self.redis_timeout,
                                             decode_responses=True)
        else:
            self.redis = StrictRedis(self.redis_host, self.redis_port, password=self.redis_password,
                                     socket_timeout=self.redis_timeout)

    def init_zookeeper(self):
        kr = KazooRetry(max_tries=-1, ignore_expire=False)
        self.zk = KazooClient(hosts=self.zk_connect, connection_retry=kr)
        self.zk.start()
        while True:  # retry when exception
            try:
                self.zk_path = self.zk.create(self.base_zk_Path + "/" + self.name,
                                              value=bytes(json.dumps(self.zk_data), 'utf8'), ephemeral=True,
                                              sequence=False, makepath=True)
                stat = self.zk.get(self.zk_path)[1]
                self.id = stat.czxid
                self.channel = "crawler:channel:" + str(self.id)
                self.logger.warning("Register crawler using node " + self.zk_path + " with zxid " + str(stat.czxid))
                break
            except NodeExistsError as e:
                self.logger.warning('Crawler of name %s already exists.', self.name)
                sleep(1)
                continue

    def destory_zookeeper(self):
        try:
            if self.zk:
                self.zk.delete(self.zk_path)
        except Exception:
            pass
        try:
            if self.zk:
                self.zk.stop()
        except Exception:
            pass

    def register(self):
        while True:  # restart forever
            try:
                self.counter = time.time()
                self.init_zookeeper()
                self.need_reset = False

                while (not self.need_reset) and self.zk.state == KazooState.CONNECTED:  # retry when exception
                    try:
                        pubsub = self.redis.pubsub()
                        pubsub.subscribe(self.channel, 'health:notify:channel')
                        self.logger.warning("Subscrible to channel " + self.channel + " and waiting for message.")

                        while (not self.need_reset) and self.zk.state == KazooState.CONNECTED:
                            item = pubsub.get_message(ignore_subscribe_messages=True, timeout=5)
                            if item and item['channel'] == self.channel:
                                json_data = item['data']
                                self.logger.warning("Receive message " + str(json_data))

                                data = json.loads(json_data)
                                if 'healthNotify' in data and str(data['healthNotify']) == '1':
                                    if data['health'] == 'DEAD' and str(data['id']) == str(self.id):
                                        self.need_reset = True
                                        self.logger.warning(
                                            'This crawler was marked DEAD by dispatcher, trying to reset.')
                                        continue
                                else:
                                    self.onMessage(data)

                            if time.time() - self.counter > 60:
                                self.zk_data['id'] = self.id
                                self.redis.publish("crawler:heartbeat:channel", json.dumps(self.zk_data));
                                self.counter = time.time()
                                self.logger.warning("Send heartbeat to dispatcher.")

                    except Exception as e:
                        self.logger.warning(traceback.format_exc())
                        sleep(1)
                        continue

                self.logger.warning('stop ....')
            except Exception as e:
                self.logger.warning(traceback.format_exc())
                sleep(1)
                continue
            finally:
                self.destory_zookeeper()

    @staticmethod
    def get_ip():
        my_name = socket.getfqdn(socket.gethostname())
        return socket.gethostbyname(my_name)

    def sendResponse(self, code, response_list, vin):
        vin_request = self.vinRequestMapper[vin]
        start_time = self.vinStartTimeMapper[vin]
        message = CrawlerRegister.messages.get(code, u"未知错误")
        #         response = json.dumps(response_list)
        response = base64.b64encode(json.dumps(response_list, ensure_ascii=False).encode('utf-8')).decode('utf-8')

        resp_data = {
            "vinRequest": vin_request,
            "response": response,
            "resourceType": self.resource_type,
            "crawlerId": self.id,
            "startTime": start_time,
            "endTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),
            "responseCode": code,
            "responseDescription": message
        }
        json_data = json.dumps(resp_data)
        self.redis.lpush('queue:response', json_data);
        del self.vinRequestMapper[vin]
        del self.vinStartTimeMapper[vin]
        self.logger.warning("Send response to dispatcher, vin: " + vin)

    def onMessage(self, data):
        vin = data['vin']
        if not vin:
            self.logger.warning('无效VIN: {0}'.format(vin))
            code = common_errors.ERR_LENGTH_VIN
            self.sendResponse(code, self.parser.get_default_result(), vin, self.parser_type)
            return
        orig_brand = data['origBrand']
        channel = data['channel']
        brand = data['brand']
        if channel != self.brand:
            self.logger.warning('不支持品牌: {0}'.format(channel))
            code = common_errors.ERR_NOT_SUPPORTED_BRAND
            self.sendResponse(code, self.parser.get_default_result(), vin, self.parser_type)
            return

        vin_info = (vin, orig_brand, channel, brand)

        self.vinRequestMapper[vin] = data
        self.vinStartTimeMapper[vin] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
        try:
            code, response_list = self.handleRequest(vin_info)
            self.sendResponse(code, response_list, vin);
        except Exception:
            self.logger.warning(traceback.format_exc())
            self.sendResponse(40, '', vin)
            raise
        #         time.sleep(random.randint(*self.delay))

    def is_integer(self, text):
        try:
            num = int(text)
            return True
        except ValueError as ve:
            return False

    @abstractmethod
    def handleRequest(self, vin_info):
        pass
